// Copyright 2015 Matthew Collins
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::ecs;
use crate::entity;
use crate::format;
use crate::protocol::{self, forge, mojang, packet};
use crate::render;
use crate::resources;
use crate::settings::Stevenkey;
use crate::shared::{Axis, Position};
use crate::types::hash::FNVHash;
use crate::types::Gamemode;
use crate::world;
use crate::world::block;
use cgmath::prelude::*;
use instant::Instant;
use log::{debug, error, info, warn};
use rand::{self, Rng};
use std::collections::HashMap;
use std::hash::BuildHasherDefault;
use std::str::FromStr;
use std::sync::mpsc;
use std::sync::{Arc, RwLock};
use std::thread;

pub mod plugin_messages;
mod sun;
pub mod target;

pub struct Server {
    uuid: protocol::UUID,
    conn: Arc<RwLock<Option<protocol::Conn>>>,
    protocol_version: i32,
    forge_mods: Vec<forge::ForgeMod>,
    read_queue: Option<mpsc::Receiver<Result<packet::Packet, protocol::Error>>>,
    pub disconnect_reason: Option<format::Component>,
    just_disconnected: bool,

    pub world: world::World,
    pub entities: ecs::Manager,
    world_age: i64,
    world_time: f64,
    world_time_target: f64,
    tick_time: bool,

    resources: Arc<RwLock<resources::Manager>>,
    version: usize,

    // Entity accessors
    game_info: ecs::Key<entity::GameInfo>,
    player_movement: ecs::Key<entity::player::PlayerMovement>,
    mouse_buttons: ecs::Key<entity::MouseButtons>,
    gravity: ecs::Key<entity::Gravity>,
    position: ecs::Key<entity::Position>,
    target_position: ecs::Key<entity::TargetPosition>,
    velocity: ecs::Key<entity::Velocity>,
    gamemode: ecs::Key<Gamemode>,
    pub rotation: ecs::Key<entity::Rotation>,
    target_rotation: ecs::Key<entity::TargetRotation>,
    //
    pub player: Option<ecs::Entity>,
    entity_map: HashMap<i32, ecs::Entity, BuildHasherDefault<FNVHash>>,
    players: HashMap<protocol::UUID, PlayerInfo, BuildHasherDefault<FNVHash>>,

    tick_timer: f64,
    entity_tick_timer: f64,
    pub received_chat_at: Option<Instant>,

    sun_model: Option<sun::SunModel>,
    target_info: target::Info,
}

#[derive(Debug)]
pub struct PlayerInfo {
    name: String,
    uuid: protocol::UUID,
    skin_url: Option<String>,

    display_name: Option<format::Component>,
    ping: i32,
    gamemode: Gamemode,
}

macro_rules! handle_packet {
    ($s:ident $pck:ident {
        $($packet:ident => $func:ident,)*
    }) => (
        match $pck {
        $(
            protocol::packet::Packet::$packet(val) => $s.$func(val),
        )*
            _ => {},
        }
    )
}

impl Server {
    pub fn connect(
        resources: Arc<RwLock<resources::Manager>>,
        profile: mojang::Profile,
        address: &str,
        protocol_version: i32,
        forge_mods: Vec<forge::ForgeMod>,
        fml_network_version: Option<i64>,
    ) -> Result<Server, protocol::Error> {
        let mut conn = protocol::Conn::new(address, protocol_version)?;

        let tag = match fml_network_version {
            Some(1) => "\0FML\0",
            Some(2) => "\0FML2\0",
            None => "",
            _ => panic!("unsupported FML network version: {:?}", fml_network_version),
        };

        let host = conn.host.clone() + tag;
        let port = conn.port;
        conn.write_packet(protocol::packet::handshake::serverbound::Handshake {
            protocol_version: protocol::VarInt(protocol_version),
            host,
            port,
            next: protocol::VarInt(2),
        })?;
        conn.state = protocol::State::Login;
        conn.write_packet(protocol::packet::login::serverbound::LoginStart {
            username: profile.username.clone(),
        })?;

        use std::rc::Rc;
        let (server_id, public_key, verify_token);
        loop {
            match conn.read_packet()? {
                protocol::packet::Packet::SetInitialCompression(val) => {
                    conn.set_compresssion(val.threshold.0);
                }
                protocol::packet::Packet::EncryptionRequest(val) => {
                    server_id = Rc::new(val.server_id);
                    public_key = Rc::new(val.public_key.data);
                    verify_token = Rc::new(val.verify_token.data);
                    break;
                }
                protocol::packet::Packet::EncryptionRequest_i16(val) => {
                    server_id = Rc::new(val.server_id);
                    public_key = Rc::new(val.public_key.data);
                    verify_token = Rc::new(val.verify_token.data);
                    break;
                }
                protocol::packet::Packet::LoginSuccess_String(val) => {
                    warn!("Server is running in offline mode");
                    debug!("Login: {} {}", val.username, val.uuid);
                    let mut read = conn.clone();
                    let mut write = conn;
                    read.state = protocol::State::Play;
                    write.state = protocol::State::Play;
                    let rx = Self::spawn_reader(read);
                    return Ok(Server::new(
                        protocol_version,
                        forge_mods,
                        protocol::UUID::from_str(&val.uuid).unwrap(),
                        resources,
                        Arc::new(RwLock::new(Some(write))),
                        Some(rx),
                    ));
                }
                protocol::packet::Packet::LoginSuccess_UUID(val) => {
                    warn!("Server is running in offline mode");
                    debug!("Login: {} {:?}", val.username, val.uuid);
                    let mut read = conn.clone();
                    let mut write = conn;
                    read.state = protocol::State::Play;
                    write.state = protocol::State::Play;
                    let rx = Self::spawn_reader(read);
                    return Ok(Server::new(
                        protocol_version,
                        forge_mods,
                        val.uuid,
                        resources,
                        Arc::new(RwLock::new(Some(write))),
                        Some(rx),
                    ));
                }
                protocol::packet::Packet::LoginDisconnect(val) => {
                    return Err(protocol::Error::Disconnect(val.reason))
                }
                val => return Err(protocol::Error::Err(format!("Wrong packet 1: {:?}", val))),
            };
        }

        let mut shared = [0; 16];
        rand::thread_rng().fill(&mut shared);

        let shared_e = rsa_public_encrypt_pkcs1::encrypt(&public_key, &shared).unwrap();
        let token_e = rsa_public_encrypt_pkcs1::encrypt(&public_key, &verify_token).unwrap();

        #[cfg(not(target_arch = "wasm32"))]
        {
            profile.join_server(&server_id, &shared, &public_key)?;
        }

        if protocol_version >= 47 {
            conn.write_packet(protocol::packet::login::serverbound::EncryptionResponse {
                shared_secret: protocol::LenPrefixedBytes::new(shared_e),
                verify_token: protocol::LenPrefixedBytes::new(token_e),
            })?;
        } else {
            conn.write_packet(
                protocol::packet::login::serverbound::EncryptionResponse_i16 {
                    shared_secret: protocol::LenPrefixedBytes::new(shared_e),
                    verify_token: protocol::LenPrefixedBytes::new(token_e),
                },
            )?;
        }

        let mut read = conn.clone();
        let mut write = conn;

        read.enable_encyption(&shared, true);
        write.enable_encyption(&shared, false);

        let uuid;
        let compression_threshold = read.compression_threshold;
        loop {
            match read.read_packet()? {
                protocol::packet::Packet::SetInitialCompression(val) => {
                    read.set_compresssion(val.threshold.0);
                    write.set_compresssion(val.threshold.0);
                }
                protocol::packet::Packet::LoginSuccess_String(val) => {
                    debug!("Login: {} {}", val.username, val.uuid);
                    uuid = protocol::UUID::from_str(&val.uuid).unwrap();
                    read.state = protocol::State::Play;
                    write.state = protocol::State::Play;
                    break;
                }
                protocol::packet::Packet::LoginSuccess_UUID(val) => {
                    debug!("Login: {} {:?}", val.username, val.uuid);
                    uuid = val.uuid;
                    read.state = protocol::State::Play;
                    write.state = protocol::State::Play;
                    break;
                }
                protocol::packet::Packet::LoginDisconnect(val) => {
                    return Err(protocol::Error::Disconnect(val.reason))
                }
                protocol::packet::Packet::LoginPluginRequest(req) => {
                    match req.channel.as_ref() {
                        "fml:loginwrapper" => {
                            let mut cursor = std::io::Cursor::new(req.data);
                            let channel: String = protocol::Serializable::read_from(&mut cursor)?;

                            let (id, mut data) = protocol::Conn::read_raw_packet_from(
                                &mut cursor,
                                compression_threshold,
                            )?;

                            match channel.as_ref() {
                                "fml:handshake" => {
                                    let packet =
                                        forge::fml2::FmlHandshake::packet_by_id(id, &mut data)?;
                                    use forge::fml2::FmlHandshake::*;
                                    match packet {
                                        ModList {
                                            mod_names,
                                            channels,
                                            registries,
                                        } => {
                                            info!("ModList mod_names={:?} channels={:?} registries={:?}", mod_names, channels, registries);
                                            write.write_fml2_handshake_plugin_message(
                                                req.message_id,
                                                Some(&ModListReply {
                                                    mod_names,
                                                    channels,
                                                    registries,
                                                }),
                                            )?;
                                        }
                                        ServerRegistry {
                                            name,
                                            snapshot_present: _,
                                            snapshot: _,
                                        } => {
                                            info!("ServerRegistry {:?}", name);
                                            write.write_fml2_handshake_plugin_message(
                                                req.message_id,
                                                Some(&Acknowledgement),
                                            )?;
                                        }
                                        ConfigurationData { filename, contents } => {
                                            info!(
                                                "ConfigurationData filename={:?} contents={}",
                                                filename,
                                                String::from_utf8_lossy(&contents)
                                            );
                                            write.write_fml2_handshake_plugin_message(
                                                req.message_id,
                                                Some(&Acknowledgement),
                                            )?;
                                        }
                                        _ => unimplemented!(),
                                    }
                                }
                                _ => panic!(
                                    "unknown LoginPluginRequest fml:loginwrapper channel: {:?}",
                                    channel
                                ),
                            }
                        }
                        _ => panic!("unsupported LoginPluginRequest channel: {:?}", req.channel),
                    }
                }
                val => return Err(protocol::Error::Err(format!("Wrong packet 2: {:?}", val))),
            }
        }

        let rx = Self::spawn_reader(read);

        Ok(Server::new(
            protocol_version,
            forge_mods,
            uuid,
            resources,
            Arc::new(RwLock::new(Some(write))),
            Some(rx),
        ))
    }

    fn spawn_reader(
        mut read: protocol::Conn,
    ) -> mpsc::Receiver<Result<packet::Packet, protocol::Error>> {
        let (tx, rx) = mpsc::channel();
        thread::spawn(move || loop {
            let pck = read.read_packet();
            let was_error = pck.is_err();
            if tx.send(pck).is_err() {
                return;
            }
            if was_error {
                return;
            }
        });
        rx
    }

    pub fn dummy_server(resources: Arc<RwLock<resources::Manager>>) -> Server {
        let mut server = Server::new(
            protocol::SUPPORTED_PROTOCOLS[0],
            vec![],
            protocol::UUID::default(),
            resources,
            Arc::new(RwLock::new(None)),
            None,
        );
        let mut rng = rand::thread_rng();
        for x in -7 * 16..7 * 16 {
            for z in -7 * 16..7 * 16 {
                let h = 5 + (6.0 * (x as f64 / 16.0).cos() * (z as f64 / 16.0).sin()) as i32;
                for y in 0..h {
                    server.world.set_block(
                        Position::new(x, y, z),
                        block::Dirt {
                            snowy: false,
                            variant: block::DirtVariant::Normal,
                        },
                    );
                }
                server
                    .world
                    .set_block(Position::new(x, h, z), block::Grass { snowy: false });

                if x * x + z * z > 16 * 16 && rng.gen_bool(1.0 / 80.0) {
                    for i in 0..5 {
                        server.world.set_block(
                            Position::new(x, h + 1 + i, z),
                            block::Log {
                                axis: Axis::Y,
                                variant: block::TreeVariant::Oak,
                            },
                        );
                    }
                    for xx in -2..3 {
                        for zz in -2..3 {
                            if xx == 0 && z == 0 {
                                continue;
                            }
                            server.world.set_block(
                                Position::new(x + xx, h + 3, z + zz),
                                block::Leaves {
                                    variant: block::TreeVariant::Oak,
                                    check_decay: false,
                                    decayable: false,
                                    distance: 1,
                                },
                            );
                            server.world.set_block(
                                Position::new(x + xx, h + 4, z + zz),
                                block::Leaves {
                                    variant: block::TreeVariant::Oak,
                                    check_decay: false,
                                    decayable: false,
                                    distance: 1,
                                },
                            );
                            if xx.abs() <= 1 && zz.abs() <= 1 {
                                server.world.set_block(
                                    Position::new(x + xx, h + 5, z + zz),
                                    block::Leaves {
                                        variant: block::TreeVariant::Oak,
                                        check_decay: false,
                                        decayable: false,
                                        distance: 1,
                                    },
                                );
                            }
                            if xx * xx + zz * zz <= 1 {
                                server.world.set_block(
                                    Position::new(x + xx, h + 6, z + zz),
                                    block::Leaves {
                                        variant: block::TreeVariant::Oak,
                                        check_decay: false,
                                        decayable: false,
                                        distance: 1,
                                    },
                                );
                            }
                        }
                    }
                }
            }
        }
        server
    }

    fn new(
        protocol_version: i32,
        forge_mods: Vec<forge::ForgeMod>,
        uuid: protocol::UUID,
        resources: Arc<RwLock<resources::Manager>>,
        conn: Arc<RwLock<Option<protocol::Conn>>>,
        read_queue: Option<mpsc::Receiver<Result<packet::Packet, protocol::Error>>>,
    ) -> Server {
        let mut entities = ecs::Manager::new();
        entity::add_systems(&mut entities);

        let world_entity = entities.get_world();
        let game_info = entities.get_key();
        entities.add_component(world_entity, game_info, entity::GameInfo::new());
        entities.add_component(world_entity, entities.get_key(), conn.clone());

        let version = resources.read().unwrap().version();
        Server {
            uuid,
            conn,
            protocol_version,
            forge_mods,
            read_queue,
            disconnect_reason: None,
            just_disconnected: false,

            world: world::World::new(protocol_version),
            world_age: 0,
            world_time: 0.0,
            world_time_target: 0.0,
            tick_time: true,

            version,
            resources,

            // Entity accessors
            game_info,
            player_movement: entities.get_key(),
            mouse_buttons: entities.get_key(),
            gravity: entities.get_key(),
            position: entities.get_key(),
            target_position: entities.get_key(),
            velocity: entities.get_key(),
            gamemode: entities.get_key(),
            rotation: entities.get_key(),
            target_rotation: entities.get_key(),
            //
            entities,
            player: None,
            entity_map: HashMap::with_hasher(BuildHasherDefault::default()),
            players: HashMap::with_hasher(BuildHasherDefault::default()),

            tick_timer: 0.0,
            entity_tick_timer: 0.0,
            received_chat_at: None,
            sun_model: None,

            target_info: target::Info::new(),
        }
    }

    pub fn disconnect(&mut self, reason: Option<format::Component>) {
        self.conn.write().unwrap().take();
        self.disconnect_reason = reason;
        if let Some(player) = self.player.take() {
            self.entities.remove_entity(player);
        }
        self.just_disconnected = true;
    }

    pub fn is_connected(&self) -> bool {
        self.conn.read().unwrap().is_some()
    }

    pub fn tick(&mut self, renderer: &mut render::Renderer, delta: f64) {
        let version = self.resources.read().unwrap().version();
        if version != self.version {
            self.version = version;
            self.world.flag_dirty_all();
        }
        // TODO: Check if the world type actually needs a sun
        if self.sun_model.is_none() {
            self.sun_model = Some(sun::SunModel::new(renderer));
        }

        // Copy to camera
        if let Some(player) = self.player {
            let position = self.entities.get_component(player, self.position).unwrap();
            let rotation = self.entities.get_component(player, self.rotation).unwrap();
            renderer.camera.pos =
                cgmath::Point3::from_vec(position.position + cgmath::Vector3::new(0.0, 1.62, 0.0));
            renderer.camera.yaw = rotation.yaw;
            renderer.camera.pitch = rotation.pitch;
        }
        self.entity_tick(renderer, delta);

        self.tick_timer += delta;
        while self.tick_timer >= 3.0 && self.is_connected() {
            self.minecraft_tick();
            self.tick_timer -= 3.0;
        }

        self.update_time(renderer, delta);

        if let Some(sun_model) = self.sun_model.as_mut() {
            sun_model.tick(renderer, self.world_time, self.world_age);
        }

        self.world.tick(&mut self.entities);

        if self.player.is_some() {
            if let Some((pos, bl, _, _)) = target::trace_ray(
                &self.world,
                4.0,
                renderer.camera.pos.to_vec(),
                renderer.view_vector.cast().unwrap(),
                target::test_block,
            ) {
                self.target_info.update(renderer, pos, bl);
            } else {
                self.target_info.clear(renderer);
            }
        } else {
            self.target_info.clear(renderer);
        }
    }

    fn entity_tick(&mut self, renderer: &mut render::Renderer, delta: f64) {
        let world_entity = self.entities.get_world();
        // Update the game's state for entities to read
        self.entities
            .get_component_mut(world_entity, self.game_info)
            .unwrap()
            .delta = delta;

        // Packets modify entities so need to handled here
        if let Some(rx) = self.read_queue.take() {
            while let Ok(pck) = rx.try_recv() {
                match pck {
                    Ok(pck) => handle_packet! {
                        self pck {
                            PluginMessageClientbound_i16 => on_plugin_message_clientbound_i16,
                            PluginMessageClientbound => on_plugin_message_clientbound_1,
                            JoinGame_WorldNames_IsHard_SimDist => on_game_join_worldnames_ishard_simdist,
                            JoinGame_WorldNames_IsHard => on_game_join_worldnames_ishard,
                            JoinGame_WorldNames => on_game_join_worldnames,
                            JoinGame_HashedSeed_Respawn => on_game_join_hashedseed_respawn,
                            JoinGame_i32_ViewDistance => on_game_join_i32_viewdistance,
                            JoinGame_i32 => on_game_join_i32,
                            JoinGame_i8 => on_game_join_i8,
                            JoinGame_i8_NoDebug => on_game_join_i8_nodebug,
                            Respawn_Gamemode => on_respawn_gamemode,
                            Respawn_HashedSeed => on_respawn_hashedseed,
                            Respawn_WorldName => on_respawn_worldname,
                            Respawn_NBT => on_respawn_nbt,
                            KeepAliveClientbound_i64 => on_keep_alive_i64,
                            KeepAliveClientbound_VarInt => on_keep_alive_varint,
                            KeepAliveClientbound_i32 => on_keep_alive_i32,
                            ChunkData_AndLight => on_chunk_data_and_light,
                            ChunkData_Biomes3D_Bitmasks => on_chunk_data_biomes3d_bitmasks,
                            ChunkData_Biomes3D_VarInt => on_chunk_data_biomes3d_varint,
                            ChunkData_Biomes3D_bool => on_chunk_data_biomes3d_bool,
                            ChunkData => on_chunk_data,
                            ChunkData_Biomes3D => on_chunk_data_biomes3d,
                            ChunkData_HeightMap => on_chunk_data_heightmap,
                            ChunkData_NoEntities => on_chunk_data_no_entities,
                            ChunkData_NoEntities_u16 => on_chunk_data_no_entities_u16,
                            ChunkData_17 => on_chunk_data_17,
                            ChunkDataBulk => on_chunk_data_bulk,
                            ChunkDataBulk_17 => on_chunk_data_bulk_17,
                            ChunkUnload => on_chunk_unload,
                            BlockChange_VarInt => on_block_change_varint,
                            BlockChange_u8 => on_block_change_u8,
                            MultiBlockChange_Packed => on_multi_block_change_packed,
                            MultiBlockChange_VarInt => on_multi_block_change_varint,
                            MultiBlockChange_u16 => on_multi_block_change_u16,
                            TeleportPlayer_WithDismount => on_teleport_player_withdismount,
                            TeleportPlayer_WithConfirm => on_teleport_player_withconfirm,
                            TeleportPlayer_NoConfirm => on_teleport_player_noconfirm,
                            TeleportPlayer_OnGround => on_teleport_player_onground,
                            TimeUpdate => on_time_update,
                            ChangeGameState => on_game_state_change,
                            UpdateBlockEntity_VarInt => on_block_entity_update_varint,
                            UpdateBlockEntity_u8 => on_block_entity_update_u8,
                            UpdateBlockEntity_Data => on_block_entity_update_data,
                            UpdateLight_Arrays => on_update_light_arrays,
                            UpdateSign => on_sign_update,
                            UpdateSign_u16 => on_sign_update_u16,
                            PlayerInfo => on_player_info,
                            PlayerInfo_String => on_player_info_string,
                            ServerMessage_NoPosition => on_servermessage_noposition,
                            ServerMessage_Position => on_servermessage_position,
                            ServerMessage_Sender => on_servermessage_sender,
                            Disconnect => on_disconnect,
                            // Entities
                            EntityDestroy => on_entity_destroy,
                            EntityDestroy_u8 => on_entity_destroy_u8,
                            SpawnPlayer_f64_NoMeta => on_player_spawn_f64_nometa,
                            SpawnPlayer_f64 => on_player_spawn_f64,
                            SpawnPlayer_i32 => on_player_spawn_i32,
                            SpawnPlayer_i32_HeldItem => on_player_spawn_i32_helditem,
                            SpawnPlayer_i32_HeldItem_String => on_player_spawn_i32_helditem_string,
                            EntityTeleport_f64 => on_entity_teleport_f64,
                            EntityTeleport_i32 => on_entity_teleport_i32,
                            EntityTeleport_i32_i32_NoGround => on_entity_teleport_i32_i32_noground,
                            EntityMove_i16 => on_entity_move_i16,
                            EntityMove_i8 => on_entity_move_i8,
                            EntityMove_i8_i32_NoGround => on_entity_move_i8_i32_noground,
                            EntityLook_VarInt => on_entity_look_varint,
                            EntityLook_i32_NoGround => on_entity_look_i32_noground,
                            EntityLookAndMove_i16 => on_entity_look_and_move_i16,
                            EntityLookAndMove_i8 => on_entity_look_and_move_i8,
                            EntityLookAndMove_i8_i32_NoGround => on_entity_look_and_move_i8_i32_noground,
                        }
                    },
                    Err(err) => panic!("Err: {:?}", err),
                }
                // Disconnected
                if self.conn.read().unwrap().is_none() {
                    break;
                }
            }

            if self.conn.read().unwrap().is_some() {
                self.read_queue = Some(rx);
            }
        }

        if self.is_connected() || self.just_disconnected {
            // Allow an extra tick when disconnected to clean up
            self.just_disconnected = false;
            self.entity_tick_timer += delta;
            while self.entity_tick_timer >= 3.0 {
                self.entities.tick(&mut self.world, renderer);
                self.entity_tick_timer -= 3.0;
            }

            self.entities.render_tick(&mut self.world, renderer);
        }
    }

    pub fn remove(&mut self, renderer: &mut render::Renderer) {
        self.entities.remove_all_entities(&mut self.world, renderer);
        if let Some(mut sun_model) = self.sun_model.take() {
            sun_model.remove(renderer);
        }
        self.target_info.clear(renderer);
    }

    fn update_time(&mut self, renderer: &mut render::Renderer, delta: f64) {
        if self.tick_time {
            self.world_time_target += delta / 3.0;
            self.world_time_target = (24000.0 + self.world_time_target) % 24000.0;
            let mut diff = self.world_time_target - self.world_time;
            if diff < -12000.0 {
                diff += 24000.0
            } else if diff > 12000.0 {
                diff -= 24000.0
            }
            self.world_time += diff * (1.5 / 60.0) * delta;
            self.world_time = (24000.0 + self.world_time) % 24000.0;
        } else {
            self.world_time = self.world_time_target;
        }
        renderer.sky_offset = self.calculate_sky_offset();
    }

    fn calculate_sky_offset(&self) -> f32 {
        use std::f32::consts::PI;
        let mut offset = ((1.0 + self.world_time as f32) / 24000.0) - 0.25;
        if offset < 0.0 {
            offset += 1.0;
        } else if offset > 1.0 {
            offset -= 1.0;
        }

        let prev_offset = offset;
        offset = 1.0 - (((offset * PI).cos() + 1.0) / 2.0);
        offset = prev_offset + (offset - prev_offset) / 3.0;

        offset = 1.0 - ((offset * PI * 2.0).cos() * 2.0 + 0.2);
        if offset > 1.0 {
            offset = 1.0;
        } else if offset < 0.0 {
            offset = 0.0;
        }
        offset = 1.0 - offset;
        offset * 0.8 + 0.2
    }

    pub fn minecraft_tick(&mut self) {
        use std::f32::consts::PI;
        if let Some(player) = self.player {
            let movement = self
                .entities
                .get_component_mut(player, self.player_movement)
                .unwrap();
            let on_ground = self
                .entities
                .get_component(player, self.gravity)
                .map_or(false, |v| v.on_ground);
            let position = self
                .entities
                .get_component(player, self.target_position)
                .unwrap();
            let rotation = self.entities.get_component(player, self.rotation).unwrap();

            // Force the server to know when touched the ground
            // otherwise if it happens between ticks the server
            // will think we are flying.
            let on_ground = if movement.did_touch_ground {
                movement.did_touch_ground = false;
                true
            } else {
                on_ground
            };

            // Sync our position to the server
            // Use the smaller packets when possible
            if self.protocol_version >= 47 {
                let packet = packet::play::serverbound::PlayerPositionLook {
                    x: position.position.x,
                    y: position.position.y,
                    z: position.position.z,
                    yaw: -(rotation.yaw as f32) * (180.0 / PI),
                    pitch: (-rotation.pitch as f32) * (180.0 / PI) + 180.0,
                    on_ground,
                };
                self.write_packet(packet);
            } else {
                let packet = packet::play::serverbound::PlayerPositionLook_HeadY {
                    x: position.position.x,
                    feet_y: position.position.y,
                    head_y: position.position.y + 1.62,
                    z: position.position.z,
                    yaw: -(rotation.yaw as f32) * (180.0 / PI),
                    pitch: (-rotation.pitch as f32) * (180.0 / PI) + 180.0,
                    on_ground,
                };
                self.write_packet(packet);
            }
        }
    }

    pub fn key_press(&mut self, down: bool, key: Stevenkey) {
        if let Some(player) = self.player {
            if let Some(movement) = self
                .entities
                .get_component_mut(player, self.player_movement)
            {
                movement.pressed_keys.insert(key, down);
            }
        }
    }

    pub fn on_left_mouse_button(&mut self, pressed: bool) {
        if let Some(player) = self.player {
            if let Some(mouse_buttons) = self.entities.get_component_mut(player, self.mouse_buttons)
            {
                mouse_buttons.left = pressed;
            }
        }
    }

    pub fn on_right_mouse_button(&mut self, pressed: bool) {
        if let Some(player) = self.player {
            if let Some(mouse_buttons) = self.entities.get_component_mut(player, self.mouse_buttons)
            {
                mouse_buttons.right = pressed;
            }
        }
    }

    pub fn on_right_click(&mut self, renderer: &mut render::Renderer) {
        use crate::shared::Direction;
        if self.player.is_some() {
            if let Some((pos, _, face, at)) = target::trace_ray(
                &self.world,
                4.0,
                renderer.camera.pos.to_vec(),
                renderer.view_vector.cast().unwrap(),
                target::test_block,
            ) {
                if self.protocol_version >= 477 {
                    self.write_packet(
                        packet::play::serverbound::PlayerBlockPlacement_insideblock {
                            location: pos,
                            face: protocol::VarInt(match face {
                                Direction::Down => 0,
                                Direction::Up => 1,
                                Direction::North => 2,
                                Direction::South => 3,
                                Direction::West => 4,
                                Direction::East => 5,
                                _ => unreachable!(),
                            }),
                            hand: protocol::VarInt(0),
                            cursor_x: at.x as f32,
                            cursor_y: at.y as f32,
                            cursor_z: at.z as f32,
                            inside_block: false,
                        },
                    );
                } else if self.protocol_version >= 315 {
                    self.write_packet(packet::play::serverbound::PlayerBlockPlacement_f32 {
                        location: pos,
                        face: protocol::VarInt(match face {
                            Direction::Down => 0,
                            Direction::Up => 1,
                            Direction::North => 2,
                            Direction::South => 3,
                            Direction::West => 4,
                            Direction::East => 5,
                            _ => unreachable!(),
                        }),
                        hand: protocol::VarInt(0),
                        cursor_x: at.x as f32,
                        cursor_y: at.y as f32,
                        cursor_z: at.z as f32,
                    });
                } else if self.protocol_version >= 49 {
                    self.write_packet(packet::play::serverbound::PlayerBlockPlacement_u8 {
                        location: pos,
                        face: protocol::VarInt(match face {
                            Direction::Down => 0,
                            Direction::Up => 1,
                            Direction::North => 2,
                            Direction::South => 3,
                            Direction::West => 4,
                            Direction::East => 5,
                            _ => unreachable!(),
                        }),
                        hand: protocol::VarInt(0),
                        cursor_x: (at.x * 16.0) as u8,
                        cursor_y: (at.y * 16.0) as u8,
                        cursor_z: (at.z * 16.0) as u8,
                    });
                } else if self.protocol_version >= 47 {
                    self.write_packet(packet::play::serverbound::PlayerBlockPlacement_u8_Item {
                        location: pos,
                        face: match face {
                            Direction::Down => 0,
                            Direction::Up => 1,
                            Direction::North => 2,
                            Direction::South => 3,
                            Direction::West => 4,
                            Direction::East => 5,
                            _ => unreachable!(),
                        },
                        hand: None,
                        cursor_x: (at.x * 16.0) as u8,
                        cursor_y: (at.y * 16.0) as u8,
                        cursor_z: (at.z * 16.0) as u8,
                    });
                } else {
                    self.write_packet(
                        packet::play::serverbound::PlayerBlockPlacement_u8_Item_u8y {
                            x: pos.x,
                            y: pos.y as u8,
                            z: pos.x,
                            face: match face {
                                Direction::Down => 0,
                                Direction::Up => 1,
                                Direction::North => 2,
                                Direction::South => 3,
                                Direction::West => 4,
                                Direction::East => 5,
                                _ => unreachable!(),
                            },
                            hand: None,
                            cursor_x: (at.x * 16.0) as u8,
                            cursor_y: (at.y * 16.0) as u8,
                            cursor_z: (at.z * 16.0) as u8,
                        },
                    );
                }
            }
        }
    }

    pub fn write_packet<T: protocol::PacketType>(&self, p: T) {
        let mut conn = self.conn.write().unwrap();
        let _ = conn.as_mut().unwrap().write_packet(p); // TODO handle errors
    }

    fn on_keep_alive_i64(
        &mut self,
        keep_alive: packet::play::clientbound::KeepAliveClientbound_i64,
    ) {
        self.write_packet(packet::play::serverbound::KeepAliveServerbound_i64 {
            id: keep_alive.id,
        });
    }

    fn on_keep_alive_varint(
        &mut self,
        keep_alive: packet::play::clientbound::KeepAliveClientbound_VarInt,
    ) {
        self.write_packet(packet::play::serverbound::KeepAliveServerbound_VarInt {
            id: keep_alive.id,
        });
    }

    fn on_keep_alive_i32(
        &mut self,
        keep_alive: packet::play::clientbound::KeepAliveClientbound_i32,
    ) {
        self.write_packet(packet::play::serverbound::KeepAliveServerbound_i32 {
            id: keep_alive.id,
        });
    }

    fn on_plugin_message_clientbound_i16(
        &mut self,
        msg: packet::play::clientbound::PluginMessageClientbound_i16,
    ) {
        self.on_plugin_message_clientbound(&msg.channel, msg.data.data.as_slice())
    }

    fn on_plugin_message_clientbound_1(
        &mut self,
        msg: packet::play::clientbound::PluginMessageClientbound,
    ) {
        self.on_plugin_message_clientbound(&msg.channel, &msg.data)
    }

    fn on_plugin_message_clientbound(&mut self, channel: &str, data: &[u8]) {
        if protocol::is_network_debug() {
            debug!(
                "Received plugin message: channel={}, data={:?}",
                channel, data
            );
        }

        match channel {
            "REGISTER" => {}   // TODO
            "UNREGISTER" => {} // TODO
            "FML|HS" => {
                let msg = crate::protocol::Serializable::read_from(&mut std::io::Cursor::new(data))
                    .unwrap();
                //debug!("FML|HS msg={:?}", msg);

                use forge::FmlHs::*;
                use forge::Phase::*;
                match msg {
                    ServerHello {
                        fml_protocol_version,
                        override_dimension,
                    } => {
                        debug!(
                            "Received FML|HS ServerHello {} {:?}",
                            fml_protocol_version, override_dimension
                        );

                        self.write_plugin_message("REGISTER", b"FML|HS\0FML\0FML|MP\0FML\0FORGE");
                        self.write_fmlhs_plugin_message(&ClientHello {
                            fml_protocol_version,
                        });
                        // Send stashed mods list received from ping packet, client matching server
                        let mods = crate::protocol::LenPrefixed::<
                            crate::protocol::VarInt,
                            forge::ForgeMod,
                        >::new(self.forge_mods.clone());
                        self.write_fmlhs_plugin_message(&ModList { mods });
                    }
                    ModList { mods } => {
                        debug!("Received FML|HS ModList: {:?}", mods);

                        self.write_fmlhs_plugin_message(&HandshakeAck {
                            phase: WaitingServerData,
                        });
                    }
                    ModIdData {
                        mappings,
                        block_substitutions: _,
                        item_substitutions: _,
                    } => {
                        debug!("Received FML|HS ModIdData");
                        for m in mappings.data {
                            let (namespace, name) = m.name.split_at(1);
                            if namespace == protocol::forge::BLOCK_NAMESPACE {
                                self.world
                                    .modded_block_ids
                                    .insert(m.id.0 as usize, name.to_string());
                            }
                        }
                        self.write_fmlhs_plugin_message(&HandshakeAck {
                            phase: WaitingServerComplete,
                        });
                    }
                    RegistryData {
                        has_more,
                        name,
                        ids,
                        substitutions: _,
                        dummies: _,
                    } => {
                        debug!("Received FML|HS RegistryData for {}", name);
                        if name == "minecraft:blocks" {
                            for m in ids.data {
                                self.world.modded_block_ids.insert(m.id.0 as usize, m.name);
                            }
                        }
                        if !has_more {
                            self.write_fmlhs_plugin_message(&HandshakeAck {
                                phase: WaitingServerComplete,
                            });
                        }
                    }
                    HandshakeAck { phase } => match phase {
                        WaitingCAck => {
                            self.write_fmlhs_plugin_message(&HandshakeAck {
                                phase: PendingComplete,
                            });
                        }
                        Complete => {
                            debug!("FML|HS handshake complete!");
                        }
                        _ => unimplemented!(),
                    },
                    _ => (),
                }
            }
            _ => (),
        }
    }

    // TODO: remove wrappers and directly call on Conn
    fn write_fmlhs_plugin_message(&mut self, msg: &forge::FmlHs) {
        let mut conn = self.conn.write().unwrap();
        let _ = conn.as_mut().unwrap().write_fmlhs_plugin_message(msg); // TODO handle errors
    }

    fn write_plugin_message(&mut self, channel: &str, data: &[u8]) {
        let mut conn = self.conn.write().unwrap();
        let _ = conn.as_mut().unwrap().write_plugin_message(channel, data); // TODO handle errors
    }

    fn on_game_join_worldnames_ishard_simdist(
        &mut self,
        join: packet::play::clientbound::JoinGame_WorldNames_IsHard_SimDist,
    ) {
        self.world.load_dimension_type(join.dimension);
        self.on_game_join(join.gamemode, join.entity_id)
    }

    fn on_game_join_worldnames_ishard(
        &mut self,
        join: packet::play::clientbound::JoinGame_WorldNames_IsHard,
    ) {
        self.world.load_dimension_type(join.dimension);
        self.on_game_join(join.gamemode, join.entity_id)
    }

    fn on_game_join_worldnames(&mut self, join: packet::play::clientbound::JoinGame_WorldNames) {
        self.on_game_join(join.gamemode, join.entity_id)
    }

    fn on_game_join_hashedseed_respawn(
        &mut self,
        join: packet::play::clientbound::JoinGame_HashedSeed_Respawn,
    ) {
        self.on_game_join(join.gamemode, join.entity_id)
    }

    fn on_game_join_i32_viewdistance(
        &mut self,
        join: packet::play::clientbound::JoinGame_i32_ViewDistance,
    ) {
        self.on_game_join(join.gamemode, join.entity_id)
    }

    fn on_game_join_i32(&mut self, join: packet::play::clientbound::JoinGame_i32) {
        self.on_game_join(join.gamemode, join.entity_id)
    }

    fn on_game_join_i8(&mut self, join: packet::play::clientbound::JoinGame_i8) {
        self.on_game_join(join.gamemode, join.entity_id)
    }

    fn on_game_join_i8_nodebug(&mut self, join: packet::play::clientbound::JoinGame_i8_NoDebug) {
        self.on_game_join(join.gamemode, join.entity_id)
    }

    fn on_game_join(&mut self, gamemode: u8, entity_id: i32) {
        let gamemode = Gamemode::from_int((gamemode & 0x7) as i32);
        let player = entity::player::create_local(&mut self.entities);
        if let Some(info) = self.players.get(&self.uuid) {
            let model = self
                .entities
                .get_component_mut_direct::<entity::player::PlayerModel>(player)
                .unwrap();
            model.set_skin(info.skin_url.clone());
        }
        *self
            .entities
            .get_component_mut(player, self.gamemode)
            .unwrap() = gamemode;
        // TODO: Temp
        self.entities
            .get_component_mut(player, self.player_movement)
            .unwrap()
            .flying = gamemode.can_fly();

        self.entity_map.insert(entity_id, player);
        self.player = Some(player);

        // Let the server know who we are
        let brand = plugin_messages::Brand {
            brand: "Steven".into(),
        };
        // TODO: refactor with write_plugin_message
        if self.protocol_version >= 47 {
            self.write_packet(brand.into_message());
        } else {
            self.write_packet(brand.into_message17());
        }
    }

    fn on_respawn_hashedseed(&mut self, respawn: packet::play::clientbound::Respawn_HashedSeed) {
        self.respawn(respawn.gamemode)
    }

    fn on_respawn_gamemode(&mut self, respawn: packet::play::clientbound::Respawn_Gamemode) {
        self.respawn(respawn.gamemode)
    }

    fn on_respawn_worldname(&mut self, respawn: packet::play::clientbound::Respawn_WorldName) {
        self.respawn(respawn.gamemode)
    }

    fn on_respawn_nbt(&mut self, respawn: packet::play::clientbound::Respawn_NBT) {
        self.respawn(respawn.gamemode)
    }

    fn respawn(&mut self, gamemode_u8: u8) {
        self.world = world::World::new(self.protocol_version);
        let gamemode = Gamemode::from_int((gamemode_u8 & 0x7) as i32);

        if let Some(player) = self.player {
            *self
                .entities
                .get_component_mut(player, self.gamemode)
                .unwrap() = gamemode;
            // TODO: Temp
            self.entities
                .get_component_mut(player, self.player_movement)
                .unwrap()
                .flying = gamemode.can_fly();
        }
    }

    fn on_disconnect(&mut self, disconnect: packet::play::clientbound::Disconnect) {
        self.disconnect(Some(disconnect.reason));
    }

    fn on_time_update(&mut self, time_update: packet::play::clientbound::TimeUpdate) {
        self.world_age = time_update.time_of_day;
        self.world_time_target = (time_update.time_of_day % 24000) as f64;
        if self.world_time_target < 0.0 {
            self.world_time_target = -self.world_time_target;
            self.tick_time = false;
        } else {
            self.tick_time = true;
        }
    }

    fn on_game_state_change(&mut self, game_state: packet::play::clientbound::ChangeGameState) {
        if game_state.reason == 3 {
            if let Some(player) = self.player {
                let gamemode = Gamemode::from_int(game_state.value as i32);
                *self
                    .entities
                    .get_component_mut(player, self.gamemode)
                    .unwrap() = gamemode;
                // TODO: Temp
                self.entities
                    .get_component_mut(player, self.player_movement)
                    .unwrap()
                    .flying = gamemode.can_fly();
            }
        }
    }

    fn on_entity_destroy(&mut self, entity_destroy: packet::play::clientbound::EntityDestroy) {
        for id in entity_destroy.entity_ids.data {
            if let Some(entity) = self.entity_map.remove(&id.0) {
                self.entities.remove_entity(entity);
            }
        }
    }

    fn on_entity_destroy_u8(
        &mut self,
        entity_destroy: packet::play::clientbound::EntityDestroy_u8,
    ) {
        for id in entity_destroy.entity_ids.data {
            if let Some(entity) = self.entity_map.remove(&id) {
                self.entities.remove_entity(entity);
            }
        }
    }

    fn on_entity_teleport_f64(
        &mut self,
        entity_telport: packet::play::clientbound::EntityTeleport_f64,
    ) {
        self.on_entity_teleport(
            entity_telport.entity_id.0,
            entity_telport.x,
            entity_telport.y,
            entity_telport.z,
            entity_telport.yaw as f64,
            entity_telport.pitch as f64,
            entity_telport.on_ground,
        )
    }

    fn on_entity_teleport_i32(
        &mut self,
        entity_telport: packet::play::clientbound::EntityTeleport_i32,
    ) {
        self.on_entity_teleport(
            entity_telport.entity_id.0,
            f64::from(entity_telport.x),
            f64::from(entity_telport.y),
            f64::from(entity_telport.z),
            entity_telport.yaw as f64,
            entity_telport.pitch as f64,
            entity_telport.on_ground,
        )
    }

    fn on_entity_teleport_i32_i32_noground(
        &mut self,
        entity_telport: packet::play::clientbound::EntityTeleport_i32_i32_NoGround,
    ) {
        let on_ground = true; // TODO: how is this supposed to be set? (for 1.7)
        self.on_entity_teleport(
            entity_telport.entity_id,
            f64::from(entity_telport.x),
            f64::from(entity_telport.y),
            f64::from(entity_telport.z),
            entity_telport.yaw as f64,
            entity_telport.pitch as f64,
            on_ground,
        )
    }

    fn on_entity_teleport(
        &mut self,
        entity_id: i32,
        x: f64,
        y: f64,
        z: f64,
        yaw: f64,
        pitch: f64,
        _on_ground: bool,
    ) {
        use std::f64::consts::PI;
        if let Some(entity) = self.entity_map.get(&entity_id) {
            let target_position = self
                .entities
                .get_component_mut(*entity, self.target_position)
                .unwrap();
            let target_rotation = self
                .entities
                .get_component_mut(*entity, self.target_rotation)
                .unwrap();
            target_position.position.x = x;
            target_position.position.y = y;
            target_position.position.z = z;
            target_rotation.yaw = -(yaw / 256.0) * PI * 2.0;
            target_rotation.pitch = -(pitch / 256.0) * PI * 2.0;
        }
    }

    fn on_entity_move_i16(&mut self, m: packet::play::clientbound::EntityMove_i16) {
        self.on_entity_move(
            m.entity_id.0,
            f64::from(m.delta_x),
            f64::from(m.delta_y),
            f64::from(m.delta_z),
        )
    }

    fn on_entity_move_i8(&mut self, m: packet::play::clientbound::EntityMove_i8) {
        self.on_entity_move(
            m.entity_id.0,
            f64::from(m.delta_x),
            f64::from(m.delta_y),
            f64::from(m.delta_z),
        )
    }

    fn on_entity_move_i8_i32_noground(
        &mut self,
        m: packet::play::clientbound::EntityMove_i8_i32_NoGround,
    ) {
        self.on_entity_move(
            m.entity_id,
            f64::from(m.delta_x),
            f64::from(m.delta_y),
            f64::from(m.delta_z),
        )
    }

    fn on_entity_move(&mut self, entity_id: i32, delta_x: f64, delta_y: f64, delta_z: f64) {
        if let Some(entity) = self.entity_map.get(&entity_id) {
            let position = self
                .entities
                .get_component_mut(*entity, self.target_position)
                .unwrap();
            position.position.x += delta_x;
            position.position.y += delta_y;
            position.position.z += delta_z;
        }
    }

    fn on_entity_look(&mut self, entity_id: i32, yaw: f64, pitch: f64) {
        use std::f64::consts::PI;
        if let Some(entity) = self.entity_map.get(&entity_id) {
            let rotation = self
                .entities
                .get_component_mut(*entity, self.target_rotation)
                .unwrap();
            rotation.yaw = -(yaw / 256.0) * PI * 2.0;
            rotation.pitch = -(pitch / 256.0) * PI * 2.0;
        }
    }

    fn on_entity_look_varint(&mut self, look: packet::play::clientbound::EntityLook_VarInt) {
        self.on_entity_look(look.entity_id.0, look.yaw as f64, look.pitch as f64)
    }

    fn on_entity_look_i32_noground(
        &mut self,
        look: packet::play::clientbound::EntityLook_i32_NoGround,
    ) {
        self.on_entity_look(look.entity_id, look.yaw as f64, look.pitch as f64)
    }

    fn on_entity_look_and_move_i16(
        &mut self,
        lookmove: packet::play::clientbound::EntityLookAndMove_i16,
    ) {
        self.on_entity_look_and_move(
            lookmove.entity_id.0,
            f64::from(lookmove.delta_x),
            f64::from(lookmove.delta_y),
            f64::from(lookmove.delta_z),
            lookmove.yaw as f64,
            lookmove.pitch as f64,
        )
    }

    fn on_entity_look_and_move_i8(
        &mut self,
        lookmove: packet::play::clientbound::EntityLookAndMove_i8,
    ) {
        self.on_entity_look_and_move(
            lookmove.entity_id.0,
            f64::from(lookmove.delta_x),
            f64::from(lookmove.delta_y),
            f64::from(lookmove.delta_z),
            lookmove.yaw as f64,
            lookmove.pitch as f64,
        )
    }

    fn on_entity_look_and_move_i8_i32_noground(
        &mut self,
        lookmove: packet::play::clientbound::EntityLookAndMove_i8_i32_NoGround,
    ) {
        self.on_entity_look_and_move(
            lookmove.entity_id,
            f64::from(lookmove.delta_x),
            f64::from(lookmove.delta_y),
            f64::from(lookmove.delta_z),
            lookmove.yaw as f64,
            lookmove.pitch as f64,
        )
    }

    fn on_entity_look_and_move(
        &mut self,
        entity_id: i32,
        delta_x: f64,
        delta_y: f64,
        delta_z: f64,
        yaw: f64,
        pitch: f64,
    ) {
        use std::f64::consts::PI;
        if let Some(entity) = self.entity_map.get(&entity_id) {
            let position = self
                .entities
                .get_component_mut(*entity, self.target_position)
                .unwrap();
            let rotation = self
                .entities
                .get_component_mut(*entity, self.target_rotation)
                .unwrap();
            position.position.x += delta_x;
            position.position.y += delta_y;
            position.position.z += delta_z;
            rotation.yaw = -(yaw / 256.0) * PI * 2.0;
            rotation.pitch = -(pitch / 256.0) * PI * 2.0;
        }
    }

    fn on_player_spawn_f64_nometa(
        &mut self,
        spawn: packet::play::clientbound::SpawnPlayer_f64_NoMeta,
    ) {
        self.on_player_spawn(
            spawn.entity_id.0,
            spawn.uuid,
            spawn.x,
            spawn.y,
            spawn.z,
            spawn.yaw as f64,
            spawn.pitch as f64,
        )
    }

    fn on_player_spawn_f64(&mut self, spawn: packet::play::clientbound::SpawnPlayer_f64) {
        self.on_player_spawn(
            spawn.entity_id.0,
            spawn.uuid,
            spawn.x,
            spawn.y,
            spawn.z,
            spawn.yaw as f64,
            spawn.pitch as f64,
        )
    }

    fn on_player_spawn_i32(&mut self, spawn: packet::play::clientbound::SpawnPlayer_i32) {
        self.on_player_spawn(
            spawn.entity_id.0,
            spawn.uuid,
            f64::from(spawn.x),
            f64::from(spawn.y),
            f64::from(spawn.z),
            spawn.yaw as f64,
            spawn.pitch as f64,
        )
    }

    fn on_player_spawn_i32_helditem(
        &mut self,
        spawn: packet::play::clientbound::SpawnPlayer_i32_HeldItem,
    ) {
        self.on_player_spawn(
            spawn.entity_id.0,
            spawn.uuid,
            f64::from(spawn.x),
            f64::from(spawn.y),
            f64::from(spawn.z),
            spawn.yaw as f64,
            spawn.pitch as f64,
        )
    }

    fn on_player_spawn_i32_helditem_string(
        &mut self,
        spawn: packet::play::clientbound::SpawnPlayer_i32_HeldItem_String,
    ) {
        // 1.7.10: populate the player list here, since we only now know the UUID
        let uuid = protocol::UUID::from_str(&spawn.uuid).unwrap();
        self.players.entry(uuid.clone()).or_insert(PlayerInfo {
            name: spawn.name.clone(),
            uuid,
            skin_url: None,

            display_name: None,
            ping: 0, // TODO: don't overwrite from PlayerInfo_String
            gamemode: Gamemode::from_int(0),
        });

        self.on_player_spawn(
            spawn.entity_id.0,
            protocol::UUID::from_str(&spawn.uuid).unwrap(),
            f64::from(spawn.x),
            f64::from(spawn.y),
            f64::from(spawn.z),
            spawn.yaw as f64,
            spawn.pitch as f64,
        )
    }

    fn on_player_spawn(
        &mut self,
        entity_id: i32,
        uuid: protocol::UUID,
        x: f64,
        y: f64,
        z: f64,
        pitch: f64,
        yaw: f64,
    ) {
        use std::f64::consts::PI;
        if let Some(entity) = self.entity_map.remove(&entity_id) {
            self.entities.remove_entity(entity);
        }
        let entity = entity::player::create_remote(
            &mut self.entities,
            self.players.get(&uuid).map_or("MISSING", |v| &v.name),
        );
        let position = self
            .entities
            .get_component_mut(entity, self.position)
            .unwrap();
        let target_position = self
            .entities
            .get_component_mut(entity, self.target_position)
            .unwrap();
        let rotation = self
            .entities
            .get_component_mut(entity, self.rotation)
            .unwrap();
        let target_rotation = self
            .entities
            .get_component_mut(entity, self.target_rotation)
            .unwrap();
        position.position.x = x;
        position.position.y = y;
        position.position.z = z;
        target_position.position.x = x;
        target_position.position.y = y;
        target_position.position.z = z;
        rotation.yaw = -(yaw / 256.0) * PI * 2.0;
        rotation.pitch = -(pitch / 256.0) * PI * 2.0;
        target_rotation.yaw = rotation.yaw;
        target_rotation.pitch = rotation.pitch;
        if let Some(info) = self.players.get(&uuid) {
            let model = self
                .entities
                .get_component_mut_direct::<entity::player::PlayerModel>(entity)
                .unwrap();
            model.set_skin(info.skin_url.clone());
        }
        self.entity_map.insert(entity_id, entity);
    }

    fn on_teleport_player_withdismount(
        &mut self,
        teleport: packet::play::clientbound::TeleportPlayer_WithDismount,
    ) {
        self.on_teleport_player(
            teleport.x,
            teleport.y,
            teleport.z,
            teleport.yaw as f64,
            teleport.pitch as f64,
            teleport.flags,
            Some(teleport.teleport_id),
        )
    }

    fn on_teleport_player_withconfirm(
        &mut self,
        teleport: packet::play::clientbound::TeleportPlayer_WithConfirm,
    ) {
        self.on_teleport_player(
            teleport.x,
            teleport.y,
            teleport.z,
            teleport.yaw as f64,
            teleport.pitch as f64,
            teleport.flags,
            Some(teleport.teleport_id),
        )
    }

    fn on_teleport_player_noconfirm(
        &mut self,
        teleport: packet::play::clientbound::TeleportPlayer_NoConfirm,
    ) {
        self.on_teleport_player(
            teleport.x,
            teleport.y,
            teleport.z,
            teleport.yaw as f64,
            teleport.pitch as f64,
            teleport.flags,
            None,
        )
    }

    fn on_teleport_player_onground(
        &mut self,
        teleport: packet::play::clientbound::TeleportPlayer_OnGround,
    ) {
        let flags: u8 = 0; // always absolute
        self.on_teleport_player(
            teleport.x,
            teleport.eyes_y - 1.62,
            teleport.z,
            teleport.yaw as f64,
            teleport.pitch as f64,
            flags,
            None,
        )
    }

    fn on_teleport_player(
        &mut self,
        x: f64,
        y: f64,
        z: f64,
        yaw: f64,
        pitch: f64,
        flags: u8,
        teleport_id: Option<protocol::VarInt>,
    ) {
        use std::f64::consts::PI;
        if let Some(player) = self.player {
            let position = self
                .entities
                .get_component_mut(player, self.target_position)
                .unwrap();
            let rotation = self
                .entities
                .get_component_mut(player, self.rotation)
                .unwrap();
            let velocity = self
                .entities
                .get_component_mut(player, self.velocity)
                .unwrap();

            position.position.x =
                calculate_relative_teleport(TeleportFlag::RelX, flags, position.position.x, x);
            position.position.y =
                calculate_relative_teleport(TeleportFlag::RelY, flags, position.position.y, y);
            position.position.z =
                calculate_relative_teleport(TeleportFlag::RelZ, flags, position.position.z, z);
            rotation.yaw = calculate_relative_teleport(
                TeleportFlag::RelYaw,
                flags,
                rotation.yaw,
                -yaw as f64 * (PI / 180.0),
            );

            rotation.pitch = -((calculate_relative_teleport(
                TeleportFlag::RelPitch,
                flags,
                (-rotation.pitch) * (180.0 / PI) + 180.0,
                pitch,
            ) - 180.0)
                * (PI / 180.0));

            if (flags & (TeleportFlag::RelX as u8)) == 0 {
                velocity.velocity.x = 0.0;
            }
            if (flags & (TeleportFlag::RelY as u8)) == 0 {
                velocity.velocity.y = 0.0;
            }
            if (flags & (TeleportFlag::RelZ as u8)) == 0 {
                velocity.velocity.z = 0.0;
            }

            if let Some(teleport_id) = teleport_id {
                self.write_packet(packet::play::serverbound::TeleportConfirm { teleport_id });
            }
        }
    }

    fn on_block_entity_update_varint(
        &mut self,
        block_update: packet::play::clientbound::UpdateBlockEntity_VarInt,
    ) {
        self.on_block_entity_update_u8(packet::play::clientbound::UpdateBlockEntity_u8 {
            location: block_update.location,
            action: block_update.action.0 as u8,
            nbt: block_update.nbt,
        });
    }

    fn on_block_entity_update_data(
        &mut self,
        _block_update: packet::play::clientbound::UpdateBlockEntity_Data,
    ) {
        // TODO: handle UpdateBlockEntity_Data for 1.7, decompress gzipped_nbt
    }

    fn on_block_entity_update_u8(
        &mut self,
        block_update: packet::play::clientbound::UpdateBlockEntity_u8,
    ) {
        match block_update.nbt {
            None => {
                // NBT is null, so we need to remove the block entity
                self.world
                    .add_block_entity_action(world::BlockEntityAction::Remove(
                        block_update.location,
                    ));
            }
            Some(nbt) => {
                match block_update.action {
                    // TODO: support more block update actions
                    //1 => // Mob spawner
                    //2 => // Command block text
                    //3 => // Beacon
                    //4 => // Mob head
                    //5 => // Conduit
                    //6 => // Banner
                    //7 => // Structure
                    //8 => // Gateway
                    9 => {
                        // Sign
                        let line1 = format::Component::from_string(
                            nbt.1.get("Text1").unwrap().as_str().unwrap(),
                        );
                        let line2 = format::Component::from_string(
                            nbt.1.get("Text2").unwrap().as_str().unwrap(),
                        );
                        let line3 = format::Component::from_string(
                            nbt.1.get("Text3").unwrap().as_str().unwrap(),
                        );
                        let line4 = format::Component::from_string(
                            nbt.1.get("Text4").unwrap().as_str().unwrap(),
                        );
                        self.world.add_block_entity_action(
                            world::BlockEntityAction::UpdateSignText(Box::new((
                                block_update.location,
                                line1,
                                line2,
                                line3,
                                line4,
                            ))),
                        );
                    }
                    //10 => // Unused
                    //11 => // Jigsaw
                    //12 => // Campfire
                    //14 => // Beehive
                    _ => {
                        debug!("Unsupported block entity action: {}", block_update.action);
                    }
                }
            }
        }
    }

    fn on_sign_update(&mut self, mut update_sign: packet::play::clientbound::UpdateSign) {
        format::convert_legacy(&mut update_sign.line1);
        format::convert_legacy(&mut update_sign.line2);
        format::convert_legacy(&mut update_sign.line3);
        format::convert_legacy(&mut update_sign.line4);
        self.world
            .add_block_entity_action(world::BlockEntityAction::UpdateSignText(Box::new((
                update_sign.location,
                update_sign.line1,
                update_sign.line2,
                update_sign.line3,
                update_sign.line4,
            ))));
    }

    fn on_sign_update_u16(&mut self, mut update_sign: packet::play::clientbound::UpdateSign_u16) {
        format::convert_legacy(&mut update_sign.line1);
        format::convert_legacy(&mut update_sign.line2);
        format::convert_legacy(&mut update_sign.line3);
        format::convert_legacy(&mut update_sign.line4);
        self.world
            .add_block_entity_action(world::BlockEntityAction::UpdateSignText(Box::new((
                Position::new(update_sign.x, update_sign.y as i32, update_sign.z),
                update_sign.line1,
                update_sign.line2,
                update_sign.line3,
                update_sign.line4,
            ))));
    }

    fn on_player_info_string(
        &mut self,
        _player_info: packet::play::clientbound::PlayerInfo_String,
    ) {
        // TODO: track online players, for 1.7.10 - this is for the <tab> online player list
        // self.players in 1.7.10 will be only spawned players (within client range)
        /*
        if player_info.online {
            self.players.entry(uuid.clone()).or_insert(PlayerInfo {
                name: player_info.name.clone(),
                uuid,
                skin_url: None,

                display_name: None,
                ping: player_info.ping as i32,
                gamemode: Gamemode::from_int(0),
            });
        } else {
            self.players.remove(&uuid);
        }
        */
    }

    fn on_player_info(&mut self, player_info: packet::play::clientbound::PlayerInfo) {
        use crate::protocol::packet::PlayerDetail::*;
        for detail in player_info.inner.players {
            match detail {
                Add {
                    name,
                    uuid,
                    properties,
                    display,
                    gamemode,
                    ping,
                } => {
                    let info = self.players.entry(uuid.clone()).or_insert(PlayerInfo {
                        name: name.clone(),
                        uuid,
                        skin_url: None,

                        display_name: display.clone(),
                        ping: ping.0,
                        gamemode: Gamemode::from_int(gamemode.0),
                    });
                    // Re-set the props of the player in case of dodgy server implementations
                    info.name = name;
                    info.display_name = display;
                    info.ping = ping.0;
                    info.gamemode = Gamemode::from_int(gamemode.0);
                    for prop in properties {
                        if prop.name != "textures" {
                            continue;
                        }
                        // Ideally we would check the signature of the blob to
                        // verify it was from Mojang and not faked by the server
                        // but this requires the public key which is distributed
                        // authlib. We could download authlib on startup and extract
                        // the key but this seems like overkill compared to just
                        // whitelisting Mojang's texture servers instead.
                        let skin_blob_result = &base64::decode(&prop.value);
                        let skin_blob = match skin_blob_result {
                            Ok(val) => val,
                            Err(err) => {
                                error!("Failed to decode skin blob, {:?}", err);
                                continue;
                            }
                        };
                        let skin_blob: serde_json::Value = match serde_json::from_slice(skin_blob) {
                            Ok(val) => val,
                            Err(err) => {
                                error!("Failed to parse skin blob, {:?}", err);
                                continue;
                            }
                        };
                        if let Some(skin_url) = skin_blob
                            .pointer("/textures/SKIN/url")
                            .and_then(|v| v.as_str())
                        {
                            info.skin_url = Some(skin_url.to_owned());
                        }
                    }

                    // Refresh our own skin when the server sends it to us.
                    // The join game packet can come before this packet meaning
                    // we may not have the skin in time for spawning ourselves.
                    // This isn't an issue for other players because this packet
                    // must come before the spawn player packet.
                    if info.uuid == self.uuid {
                        let model = self
                            .entities
                            .get_component_mut_direct::<entity::player::PlayerModel>(
                                self.player.unwrap(),
                            )
                            .unwrap();
                        model.set_skin(info.skin_url.clone());
                    }
                }
                UpdateGamemode { uuid, gamemode } => {
                    if let Some(info) = self.players.get_mut(&uuid) {
                        info.gamemode = Gamemode::from_int(gamemode.0);
                    }
                }
                UpdateLatency { uuid, ping } => {
                    if let Some(info) = self.players.get_mut(&uuid) {
                        info.ping = ping.0;
                    }
                }
                UpdateDisplayName { uuid, display } => {
                    if let Some(info) = self.players.get_mut(&uuid) {
                        info.display_name = display;
                    }
                }
                Remove { uuid } => {
                    self.players.remove(&uuid);
                }
            }
        }
    }

    fn on_servermessage_noposition(
        &mut self,
        m: packet::play::clientbound::ServerMessage_NoPosition,
    ) {
        self.on_servermessage(&m.message, None, None);
    }

    fn on_servermessage_position(&mut self, m: packet::play::clientbound::ServerMessage_Position) {
        self.on_servermessage(&m.message, Some(m.position), None);
    }

    fn on_servermessage_sender(&mut self, m: packet::play::clientbound::ServerMessage_Sender) {
        self.on_servermessage(&m.message, Some(m.position), Some(m.sender));
    }

    fn on_servermessage(
        &mut self,
        message: &format::Component,
        _position: Option<u8>,
        _sender: Option<protocol::UUID>,
    ) {
        info!("Received chat message: {}", message);
        self.received_chat_at = Some(Instant::now());
    }

    fn load_block_entities(&mut self, block_entities: Vec<Option<crate::nbt::NamedTag>>) {
        for block_entity in block_entities.into_iter().flatten() {
            let x = block_entity.1.get("x").unwrap().as_int().unwrap();
            let y = block_entity.1.get("y").unwrap().as_int().unwrap();
            let z = block_entity.1.get("z").unwrap().as_int().unwrap();
            if let Some(tile_id) = block_entity.1.get("id") {
                let tile_id = tile_id.as_str().unwrap();
                let action = match tile_id {
                    // Fake a sign update
                    "Sign" => 9,
                    // Not something we care about, so break the loop
                    _ => continue,
                };
                self.on_block_entity_update_u8(packet::play::clientbound::UpdateBlockEntity_u8 {
                    location: Position::new(x, y, z),
                    action,
                    nbt: Some(block_entity.clone()),
                });
            } else {
                warn!(
                    "Block entity at ({},{},{}) missing id tag: {:?}",
                    x, y, z, block_entity
                );
            }
        }
    }

    fn on_chunk_data_and_light(
        &mut self,
        chunk_data: packet::play::clientbound::ChunkData_AndLight,
    ) {
        self.world
            .load_chunk118(
                chunk_data.chunk_x,
                chunk_data.chunk_z,
                true,
                chunk_data.data.data,
            )
            .unwrap();
        //self.load_block_entities(chunk_data.block_entities.data); // TODO: load entities

        // Set block light data
        self.world.set_light_data(
            chunk_data.chunk_x,
            chunk_data.chunk_z,
            world::LightType::Block,
            chunk_data.block_light_mask.data,
            chunk_data.block_light_arrays.data,
        );

        // Set sky light data
        self.world.set_light_data(
            chunk_data.chunk_x,
            chunk_data.chunk_z,
            world::LightType::Sky,
            chunk_data.sky_light_mask.data,
            chunk_data.sky_light_arrays.data,
        );

        // Clear block light data
        self.world.clear_light_data(
            chunk_data.chunk_x,
            chunk_data.chunk_z,
            world::LightType::Block,
            chunk_data.empty_block_light_mask.data,
        );

        // Clear sky light data
        self.world.clear_light_data(
            chunk_data.chunk_x,
            chunk_data.chunk_z,
            world::LightType::Sky,
            chunk_data.empty_sky_light_mask.data,
        );
    }

    fn on_chunk_data_biomes3d_bitmasks(
        &mut self,
        chunk_data: packet::play::clientbound::ChunkData_Biomes3D_Bitmasks,
    ) {
        self.world
            .load_chunk117(
                chunk_data.chunk_x,
                chunk_data.chunk_z,
                true,
                chunk_data.bitmasks.data[0] as u64, // TODO: get all bitmasks
                chunk_data.data.data,
            )
            .unwrap();
        self.load_block_entities(chunk_data.block_entities.data);
    }

    fn on_chunk_data_biomes3d_varint(
        &mut self,
        chunk_data: packet::play::clientbound::ChunkData_Biomes3D_VarInt,
    ) {
        self.world
            .load_chunk115(
                chunk_data.chunk_x,
                chunk_data.chunk_z,
                chunk_data.new,
                chunk_data.bitmask.0 as u16,
                chunk_data.data.data,
            )
            .unwrap();
        self.load_block_entities(chunk_data.block_entities.data);
    }

    fn on_chunk_data_biomes3d_bool(
        &mut self,
        chunk_data: packet::play::clientbound::ChunkData_Biomes3D_bool,
    ) {
        self.world
            .load_chunk115(
                chunk_data.chunk_x,
                chunk_data.chunk_z,
                chunk_data.new,
                chunk_data.bitmask.0 as u16,
                chunk_data.data.data,
            )
            .unwrap();
        self.load_block_entities(chunk_data.block_entities.data);
    }

    fn on_chunk_data_biomes3d(
        &mut self,
        chunk_data: packet::play::clientbound::ChunkData_Biomes3D,
    ) {
        self.world
            .load_chunk115(
                chunk_data.chunk_x,
                chunk_data.chunk_z,
                chunk_data.new,
                chunk_data.bitmask.0 as u16,
                chunk_data.data.data,
            )
            .unwrap();
        self.load_block_entities(chunk_data.block_entities.data);
    }

    fn on_chunk_data(&mut self, chunk_data: packet::play::clientbound::ChunkData) {
        self.world
            .load_chunk19(
                chunk_data.chunk_x,
                chunk_data.chunk_z,
                chunk_data.new,
                chunk_data.bitmask.0 as u16,
                chunk_data.data.data,
            )
            .unwrap();
        self.load_block_entities(chunk_data.block_entities.data);
    }

    fn on_chunk_data_heightmap(
        &mut self,
        chunk_data: packet::play::clientbound::ChunkData_HeightMap,
    ) {
        self.world
            .load_chunk19(
                chunk_data.chunk_x,
                chunk_data.chunk_z,
                chunk_data.new,
                chunk_data.bitmask.0 as u16,
                chunk_data.data.data,
            )
            .unwrap();
        self.load_block_entities(chunk_data.block_entities.data);
    }

    fn on_chunk_data_no_entities(
        &mut self,
        chunk_data: packet::play::clientbound::ChunkData_NoEntities,
    ) {
        self.world
            .load_chunk19(
                chunk_data.chunk_x,
                chunk_data.chunk_z,
                chunk_data.new,
                chunk_data.bitmask.0 as u16,
                chunk_data.data.data,
            )
            .unwrap();
    }

    fn on_chunk_data_no_entities_u16(
        &mut self,
        chunk_data: packet::play::clientbound::ChunkData_NoEntities_u16,
    ) {
        let chunk_meta = vec![crate::protocol::packet::ChunkMeta {
            x: chunk_data.chunk_x,
            z: chunk_data.chunk_z,
            bitmask: chunk_data.bitmask,
        }];
        let skylight = false;
        self.world
            .load_chunks18(chunk_data.new, skylight, &chunk_meta, chunk_data.data.data)
            .unwrap();
    }

    fn on_chunk_data_17(&mut self, chunk_data: packet::play::clientbound::ChunkData_17) {
        self.world
            .load_chunk17(
                chunk_data.chunk_x,
                chunk_data.chunk_z,
                chunk_data.new,
                chunk_data.bitmask,
                chunk_data.add_bitmask,
                chunk_data.compressed_data.data,
            )
            .unwrap();
    }

    fn on_chunk_data_bulk(&mut self, bulk: packet::play::clientbound::ChunkDataBulk) {
        let new = true;
        self.world
            .load_chunks18(
                new,
                bulk.skylight,
                &bulk.chunk_meta.data,
                bulk.chunk_data.to_vec(),
            )
            .unwrap();
    }

    fn on_chunk_data_bulk_17(&mut self, bulk: packet::play::clientbound::ChunkDataBulk_17) {
        self.world
            .load_chunks17(
                bulk.chunk_column_count,
                bulk.data_length,
                bulk.skylight,
                &bulk.chunk_data_and_meta,
            )
            .unwrap();
    }

    fn on_chunk_unload(&mut self, chunk_unload: packet::play::clientbound::ChunkUnload) {
        self.world
            .unload_chunk(chunk_unload.x, chunk_unload.z, &mut self.entities);
    }

    fn on_block_change(&mut self, location: Position, id: i32) {
        self.world.set_block(
            location,
            self.world
                .id_map
                .by_vanilla_id(id as usize, &self.world.modded_block_ids),
        )
    }

    fn on_block_change_varint(
        &mut self,
        block_change: packet::play::clientbound::BlockChange_VarInt,
    ) {
        self.on_block_change(block_change.location, block_change.block_id.0)
    }

    fn on_block_change_u8(&mut self, block_change: packet::play::clientbound::BlockChange_u8) {
        self.on_block_change(
            crate::shared::Position::new(block_change.x, block_change.y as i32, block_change.z),
            (block_change.block_id.0 << 4) | (block_change.block_metadata as i32),
        );
    }

    fn on_multi_block_change_packed(
        &mut self,
        block_change: packet::play::clientbound::MultiBlockChange_Packed,
    ) {
        let sx = (block_change.chunk_section_pos >> 42) as i32;
        let sy = ((block_change.chunk_section_pos << 44) >> 44) as i32;
        let sz = ((block_change.chunk_section_pos << 22) >> 42) as i32;

        for record in block_change.records.data {
            let block_raw_id = record.0 >> 12;
            let lz = (record.0 & 0xf) as i32;
            let ly = ((record.0 >> 4) & 0xf) as i32;
            let lx = ((record.0 >> 8) & 0xf) as i32;

            self.world.set_block(
                Position::new(sx + lx as i32, sy + ly as i32, sz + lz as i32),
                self.world
                    .id_map
                    .by_vanilla_id(block_raw_id as usize, &self.world.modded_block_ids),
            );
        }
    }

    fn on_multi_block_change_varint(
        &mut self,
        block_change: packet::play::clientbound::MultiBlockChange_VarInt,
    ) {
        let ox = block_change.chunk_x << 4;
        let oz = block_change.chunk_z << 4;
        for record in block_change.records.data {
            self.world.set_block(
                Position::new(
                    ox + (record.xz >> 4) as i32,
                    record.y as i32,
                    oz + (record.xz & 0xF) as i32,
                ),
                self.world
                    .id_map
                    .by_vanilla_id(record.block_id.0 as usize, &self.world.modded_block_ids),
            );
        }
    }

    fn on_multi_block_change_u16(
        &mut self,
        block_change: packet::play::clientbound::MultiBlockChange_u16,
    ) {
        let ox = block_change.chunk_x << 4;
        let oz = block_change.chunk_z << 4;

        let mut data = std::io::Cursor::new(block_change.data);

        for _ in 0..block_change.record_count {
            use byteorder::{BigEndian, ReadBytesExt};

            let record = data.read_u32::<BigEndian>().unwrap();

            let id = record & 0x0000_ffff;
            let y = ((record & 0x00ff_0000) >> 16) as i32;
            let z = oz + ((record & 0x0f00_0000) >> 24) as i32;
            let x = ox + ((record & 0xf000_0000) >> 28) as i32;

            self.world.set_block(
                Position::new(x, y, z),
                self.world
                    .id_map
                    .by_vanilla_id(id as usize, &self.world.modded_block_ids),
            );
        }
    }

    fn on_update_light_arrays(
        &mut self,
        light_update: packet::play::clientbound::UpdateLight_Arrays,
    ) {
        // Clear block light data
        self.world.clear_light_data(
            light_update.chunk_x.0,
            light_update.chunk_z.0,
            world::LightType::Block,
            light_update.empty_block_light_mask.data,
        );

        // Clear sky light data
        self.world.clear_light_data(
            light_update.chunk_x.0,
            light_update.chunk_z.0,
            world::LightType::Sky,
            light_update.empty_sky_light_mask.data,
        );

        // Set block light data
        self.world.set_light_data(
            light_update.chunk_x.0,
            light_update.chunk_z.0,
            world::LightType::Block,
            light_update.block_light_mask.data,
            light_update.block_light_arrays.data,
        );

        // Set sky light data
        self.world.set_light_data(
            light_update.chunk_x.0,
            light_update.chunk_z.0,
            world::LightType::Sky,
            light_update.sky_light_mask.data,
            light_update.sky_light_arrays.data,
        );
    }
}

#[allow(clippy::enum_variant_names)]
#[derive(Debug, Clone, Copy)]
enum TeleportFlag {
    RelX = 0b00001,
    RelY = 0b00010,
    RelZ = 0b00100,
    RelYaw = 0b01000,
    RelPitch = 0b10000,
}

fn calculate_relative_teleport(flag: TeleportFlag, flags: u8, base: f64, val: f64) -> f64 {
    if (flags & (flag as u8)) == 0 {
        val
    } else {
        base + val
    }
}
